/**
 * 
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 *   
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * 
 */
package org.jiopi.ibean.kernel.context.classloader;

import java.net.URL;
import java.util.*;

import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.jiopi.framework.core.JiopiConfigConstants;
import org.jiopi.ibean.kernel.config.JIOPiConfig;
import org.jiopi.ibean.kernel.context.ContextCentralConsoleKernel;
import org.jiopi.ibean.kernel.context.ControlPanelImpl;
import org.jiopi.ibean.kernel.context.ModuleConsoleImpl;
import org.jiopi.ibean.kernel.util.ObjectAccessor;
import org.jiopi.ibean.kernel.util.XMLMerger;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.LoaderClassPath;
import javassist.Modifier;
import javassist.CtNewMethod;
import javassist.NotFoundException;

/**
 * 
 * load class and redefine it
 * 
 * @version 0.4 2010.6.9
 * @since iBeanKernel0.4 2010.6.9
 *
 */
class RedefineableClassLoader {
	
	private static Logger logger = Logger.getLogger(RedefineableClassLoader.class);
	
	private final ClassPool pool;
	
	protected final ContextCentralConsoleKernel ccck;
	
	protected final ClassLoader parentClassLoader;
	
	private final HashMap<String,RedefineClass> redefineInfos;
	
	private final HashMap<String,Class<?>> redefineClasses = new HashMap<String,Class<?>>();
	
	protected final static String CONTROL_PANEL_FIELD = "__JIOPI_CONTROL_PANEL_FIELD__";
	protected final static String CONTROL_PANEL_CLASS_FIELD = "__JIOPI_CONTROL_PANEL_CLASS_FIELD__";
	

	protected RedefineableClassLoader(ClassLoader cl,ContextCentralConsoleKernel ccck,HashMap<String,RedefineClass> rc){
		pool = new ClassPool();
		pool.appendClassPath( new LoaderClassPath( cl ) );
		parentClassLoader = cl;
		redefineInfos = rc;
		this.ccck = ccck;
	}
	
	public static RedefineableClassLoader getRedefineableClassLoader(IBeanClassLoader ibcl){
		HashMap<String,RedefineClass> redefineClasses = getRedefineClasses(ibcl.getLocalResource(JiopiConfigConstants.ASSEMBLING_FILE));
		if(redefineClasses!=null) return new RedefineableClassLoader(ibcl,ibcl.ccck,redefineClasses);
		return null;
	}
	
	public synchronized Class<?> loadClass(String name){
		RedefineClass rc = redefineInfos.get(name);
		Class<?> c = null;
		if(rc!=null){
			c = redefineClasses.get(name);
			if(c==null){
				LoaderClassPath loaderClassPath = null;
				try{
					CtClass ctClass = null;
					ClassLoader ccl = Thread.currentThread().getContextClassLoader();
					try{
						Thread.currentThread().setContextClassLoader(parentClassLoader);
						ctClass = pool.getCtClass( name );
					}finally{
						Thread.currentThread().setContextClassLoader(ccl);
					}
					
					//get Redefine Class Info
					ClassLoader ocl = ModuleConsoleImpl.originContextClassLoader.get();
					ClassLoader contextClassLoader = null;
					ControlPanelImpl cpi = null;
					ModuleConsoleImpl toModuleConsole = null;
					try{
						if(ocl!=null){
							contextClassLoader = Thread.currentThread().getContextClassLoader();
							Thread.currentThread().setContextClassLoader(ocl);
						}
						toModuleConsole = ccck.accessModuleConsole(rc.moduleName, rc.version); 
						cpi = toModuleConsole.accessStaticControlPanel(rc.registerName);
					}finally{
						if(contextClassLoader!=null)Thread.currentThread().setContextClassLoader( contextClassLoader );
					}
					if(cpi!=null){
						Class<?> toClassAPI = toModuleConsole.getClassLoader().loadBlueprintClass(rc.controlpanel);

						loaderClassPath = new LoaderClassPath(toModuleConsole.getClassLoader());
						this.pool.appendClassPath(loaderClassPath);
						
						Class<?> toClass = (Class<?>)cpi.wrapped;
						//System.out.println(">>>toClass>>>"+toClass);
						//System.out.println(toModuleConsole.getClassLoader()+">toClassAPI>>"+toClassAPI.getName()+">"+toClassAPI.getClassLoader()+">"+((JIOPiJarClassLoader)toClassAPI.getClassLoader()).blueprintName);
						//System.out.println(">>tpppp>>"+toClass.getSuperclass().getClassLoader());
						//create reference field
//						CtField oldField = ctClass.getDeclaredField(CONTROL_PANEL_FIELD);
//						if(oldField!=null)ctClass.removeField(oldField);
//						
//						oldField = ctClass.getDeclaredField(CONTROL_PANEL_CLASS_FIELD);
//						if(oldField!=null)ctClass.removeField(oldField);
						
						CtField field = null;
						try {
							field = ctClass.getField(CONTROL_PANEL_FIELD);
						} catch (NotFoundException ignored) {
						}
						if(field == null){
							field = CtField.make( "private "+toClassAPI.getName()+" "+CONTROL_PANEL_FIELD+";", ctClass );
							ctClass.addField( field );
						}
						
						CtField fieldc = null;
						try {
							fieldc = ctClass.getField(CONTROL_PANEL_CLASS_FIELD);
						} catch (NotFoundException ignored) {
						}
						if(fieldc == null){
							fieldc = CtField.make( "private static Class "+CONTROL_PANEL_CLASS_FIELD+";", ctClass );
							ctClass.addField( fieldc );
						}
						
						boolean needInstantiation;
						//check if has Instantiation annotation
						needInstantiation = (toModuleConsole.moduleClassLoader.getInstantiationConfig(toClass.getName())!=null);
						//check if has control panel configuration
						if(!needInstantiation){
							URL assemblingConfigFile = parentClassLoader.getResource(JiopiConfigConstants.ASSEMBLING_FILE);
							if(assemblingConfigFile!=null){
								Document assemblingConfigDocument = XMLMerger.readDocumentAndCloseStream(assemblingConfigFile.openStream());
								if(assemblingConfigDocument != null){
									JIOPiConfig assemblingConfig = new JIOPiConfig(assemblingConfigDocument);
									JIOPiConfig.Module findModuleConfig = assemblingConfig.getModuleConfig(rc.moduleName, rc.version);
									needInstantiation = (findModuleConfig.getMatchedControlpanel(toModuleConsole.getClassLoader(),rc.controlpanel,rc.configuration)!=null);
								}
							}
						}
						
						
						//change Constructors
						CtConstructor[] ctConstructors = ctClass.getDeclaredConstructors();
						for( CtConstructor ctcons:ctConstructors ) {
							int modifiers = ctcons.getModifiers();
							if(Modifier.isPublic(modifiers)){
								if(needInstantiation){
									//get real obj at runtime
									ctcons.setBody("$0."+CONTROL_PANEL_FIELD+"=("+toClassAPI.getName()+")org.jiopi.framework.CentralConsole.accessControlPanel( \""+rc.moduleName+"\",\""+rc.version+"\" , \""+rc.registerName+"\", "+toClassAPI.getName()+".class, $args );");
									//ctcons.setBody("{System.out.println("+toClassAPI.getName()+".class.getClassLoader());System.out.println(org.jiopi.framework.CentralConsole.accessControlPanel( \""+rc.moduleName+"\",\""+rc.version+"\" , \""+rc.controlpanel+"\", "+toClassAPI.getName()+".class, $args ).getClass().getSuperclass().getClassLoader());}");
								}else{
									ctcons.setBody("{ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();try{Thread.currentThread().setContextClassLoader("+CONTROL_PANEL_CLASS_FIELD+".getClassLoader());$0."+CONTROL_PANEL_FIELD+"=("+toClassAPI.getName()+")"+CONTROL_PANEL_CLASS_FIELD+".getConstructor($sig).newInstance($args);}catch(Exception e){throw new RuntimeException(e);}finally{Thread.currentThread().setContextClassLoader( contextClassLoader );}}");
								}
							}
						}
						
						{//change methods
							Set<CtMethod>  declaredSet = getDeclaredMethodSet( ctClass );
							CtMethod[] methods = ctClass.getMethods();
							boolean modify;
							for( CtMethod method:methods ) {
								modify = false;
								int modifiers = method.getModifiers();
								if(Modifier.isPublic(modifiers)) {//only redefine public		
									if( declaredSet.contains( method )) {
										if(haveMethod(toClassAPI, method))modify = true;
									}else{
										if( !Modifier.isNative( modifiers ) 
												&& !Modifier.isFinal( modifiers )
												&& !Modifier.isAbstract( modifiers ) ) {
											method = CtNewMethod.copy( method, ctClass, null );		// added
											ctClass.addMethod( method );
											modify = true;
										}
									}
								}
								if( modify ) {
									String methodName = method.getName();
									if(Modifier.isStatic(modifiers)){
										method.setBody("return ($r)"+CONTROL_PANEL_CLASS_FIELD+".getMethod(\""+methodName+"\",$sig).invoke(null,$args);");
									}else{
										method.setBody( "return "+"$0."+CONTROL_PANEL_FIELD+"."+methodName+"($$);" );
									}
								}
							}
						}
						
						//Change public static field
						ArrayList<String> changeStaticList = new ArrayList<String>();
						{
							Set<CtField>  declaredSet = getDeclaredFieldSet( ctClass );
							CtField[] fields = ctClass.getFields();
							for(CtField ctField:fields){
								int modifiers = ctField.getModifiers();
								//System.out.println("check "+ctField.getName() +" ("+haveField(toClassAPI,ctField)+") at "+toClassAPI);
								if(Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers) && haveField(toClassAPI,ctField)){
									if(!declaredSet.contains(ctField)){
										CtField newCtField = null;
										try{
											newCtField = ctClass.getField(ctField.getName());
										}catch(NotFoundException ignored){
										}
										if(newCtField == null){
											newCtField = new CtField(ctField,ctClass);
											ctClass.addField(newCtField);
										}
									}
									changeStaticList.add(ctField.getName());
								}
							}
						}
						
						contextClassLoader = Thread.currentThread().getContextClassLoader();
						try{
							Thread.currentThread().setContextClassLoader(parentClassLoader);
							c = ctClass.toClass();
						}finally{
							Thread.currentThread().setContextClassLoader(contextClassLoader);
						}
						if(c!=null){
							ObjectAccessor.Accessible.setVariable(c, CONTROL_PANEL_CLASS_FIELD, toClass);
							//System.out.println(">>>>>"+ObjectAccessor.Accessible.variable(c, CONTROL_PANEL_CLASS_FIELD, null));
							for(String staticField:changeStaticList){
								ObjectAccessor.setVariable(c, staticField, ObjectAccessor.variable(toClass, staticField, null));
							}
						}
					}
				}catch(Exception e){
					logger.error("redefine class "+name+" error ! to "+rc.toString(),e);
				}finally{
					if(loaderClassPath!=null)this.pool.removeClassPath(loaderClassPath);
				}
				redefineClasses.put(name, c);
			}
		}
		return c;
	}
	
	protected static Set<CtField> getDeclaredFieldSet( CtClass ctClass ) {
		CtField[] declared = ctClass.getDeclaredFields();
		Set<CtField> declaredSet = new HashSet<CtField>();
		if( declared!=null ) {
            declaredSet.addAll(Arrays.asList(declared));
		}
		return declaredSet;
	}
	
	protected static Set<CtMethod> getDeclaredMethodSet( CtClass ctClass ) {
		CtMethod[] declared = ctClass.getDeclaredMethods();
		Set<CtMethod> declaredSet = new HashSet<CtMethod>();
		if( declared!=null ) {
            declaredSet.addAll(Arrays.asList(declared));
		}
		return declaredSet;
	}
	
	/**
	 * get Redefine Class in jiopiConfig file
	 * @param jiopiConfigURL jiopi config URL
	 * @return Redefine Class info
	 */
	@SuppressWarnings("unchecked")
	protected static HashMap<String,RedefineClass> getRedefineClasses(URL jiopiConfigURL){
		if(jiopiConfigURL == null)return null;
		
		try{
			SAXReader reader = new SAXReader();
			Document jiopiConfig = reader.read(jiopiConfigURL);
			Element configurationElement = jiopiConfig.getRootElement();
			Iterator<Element> redefineClassElements =  configurationElement.elementIterator("redefine");
			HashMap<String,RedefineableClassLoader.RedefineClass> redefineClasses = new HashMap<String,RedefineClass>();
			while(redefineClassElements.hasNext()){
				Element redefineClassElement = redefineClassElements.next();
				String className = redefineClassElement.attributeValue("class");
				if(className!=null && className.length()>0){
					String module = redefineClassElement.attributeValue("module");
					String version = redefineClassElement.attributeValue("version");
					String controlpanel = redefineClassElement.attributeValue("controlpanel");
					String configuration = redefineClassElement.attributeValue("configuration");
					RedefineClass rc = new RedefineClass(module,version,controlpanel,configuration);
					redefineClasses.put(className, rc);
				}
			}
			if(redefineClasses.size()>0)return redefineClasses;
		}catch(Exception e){
			logger.error("load Redefine Class configuration Error!",e);
		}
		
		return null;
	}
	
	public static class RedefineClass{
		public final String moduleName;
		public final String version;
		public final String controlpanel;
		public final String configuration;
		public final String registerName;
		private RedefineClass(String m,String v,String cp,String c){
			moduleName = m;
			version = v;
			controlpanel = cp;
			configuration = c;
			registerName = ModuleConsoleImpl.mergeRegisterName(controlpanel, configuration);
		}
		
		public String toString(){
			return "[moduleName:"+moduleName+",version:"+version+",controlpanel:"+controlpanel+",configuration:"+configuration+"]";
		}
	}
	
	protected static boolean haveField(Class<?>API,CtField field){
		try{
			//System.out.println("check  haveField "+field.getName()+" at API "+API);
			Class<?> fieldType = API.getField(field.getName()).getType();
			//System.out.println("fieldType:"+fieldType);
			ClassLoader cl = API.getClassLoader();
			if(cl == null) cl = ClassLoader.getSystemClassLoader();
			Class<?> testFieldType = ObjectAccessor.loadClass(field.getType().getName(),cl);
			//System.out.println((fieldType == testFieldType));
			return (fieldType == testFieldType);
		}catch(Exception ignored){
            
		}
		
		return false;
	}
	
	protected static boolean haveMethod(Class<?>API,CtMethod method){
		
		try{
			CtClass[] ctParameters = method.getParameterTypes();
			Class<?>[] testParameters = new Class<?>[ctParameters.length];
			ClassLoader cl = API.getClassLoader();
			if(cl == null) cl = ClassLoader.getSystemClassLoader();
			for(int i=0;i<ctParameters.length;i++){
				testParameters[i] = ObjectAccessor.loadClass(ctParameters[i].getName(),cl); 
			}
			return API.getMethod(method.getName(), testParameters) != null;
		}catch(Exception ignored){
            
		}
		return false;
	}
}
